////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                                                                                                            //
//                                                         WNProject                                                          //
//                                                                                                                            //
//            This file is distributed under the BSD 2-Clause open source license. See License.txt for details.               //
//                                                                                                                            //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#pragma once

#ifndef __WN_CONCURRENCY_THREAD_INL__
#define __WN_CONCURRENCY_THREAD_INL__

#include "WNCore/inc/WNAssert.h"
#include "WNCore/inc/WNSystem.h"
#include "WNMemory/inc/WNAllocation.h"

#ifdef _WN_WINDOWS
    #include <windows.h>
#elif defined _WN_LINUX
    #include "WNCore/inc/WNMacros.h"

    #include <unistd.h>
    #include <pthread.h>
    #include <time.h>
    #include <sys/syscall.h>
    #include <errno.h>
    #include <sched.h>
#endif

namespace WNConcurrency {
    WN_FORCE_INLINE WNThread::WNThread() :
        #ifdef _WN_WINDOWS
            mHandle(NULL),
        #endif
        mData(WN_NULL) {
    }

    WN_FORCE_INLINE WNThread::WNThread(const WNThread& _thread) :
        #ifdef _WN_WINDOWS
            mHandle(NULL),
        #endif
        mData(WN_NULL) {
        if (_thread.mData != WN_NULL) {
            _thread.mData->AddRef();

            mData = _thread.mData;
        }

        #ifdef _WN_WINDOWS
            if (_thread.mHandle != NULL) {
                const HANDLE process = GetCurrentProcess();

                WN_RELEASE_ASSERT(DuplicateHandle(process, _thread.mHandle, process, &mHandle, 0, FALSE, DUPLICATE_SAME_ACCESS));
            }
        #endif
    }

    template <typename Return>
    WN_FORCE_INLINE WNThread::WNThread(Return (*_function)()) :
        #ifdef _WN_WINDOWS
            mHandle(NULL),
        #endif
        mData(WN_NULL) {
        Create<Return, WN_VOID>(reinterpret_cast<WN_VOID*>(_function), WN_NULL);
    }

    template <typename Return, typename Parameter>
    WN_FORCE_INLINE WNThread::WNThread(Return (*_function)(Parameter), Parameter _parameter) :
        #ifdef _WN_WINDOWS
            mHandle(NULL),
        #endif
        mData(WN_NULL) {

        Parameter* parameter = WNMemory::WNMallocT<Parameter>();

        *parameter = _parameter;

        Create<Return, Parameter>(reinterpret_cast<WN_VOID*>(_function), reinterpret_cast<WN_VOID*>(parameter));
    }

    WN_FORCE_INLINE WNThread::~WNThread() {
        #ifdef _WN_WINDOWS
            if (mHandle != NULL) {
                WN_RELEASE_ASSERT(CloseHandle(mHandle));
            }
        #endif

        if (mData != WN_NULL) {
            if (mData->RemoveRef()) {
                WN_DELETE(mData);
            }
        }
    }

    WN_FORCE_INLINE WNThread& WNThread::operator = (const WNThread& _thread) {
        if (_thread.mData != WN_NULL) {
            _thread.mData->AddRef();

            if (mData != WN_NULL) {
                if (mData->RemoveRef()) {
                    WN_DELETE(mData);
                }
            }

            mData = _thread.mData;
        }

        #ifdef _WN_WINDOWS
            if (_thread.mHandle != NULL) {
                const HANDLE process = GetCurrentProcess();
                HANDLE handle = NULL;

                WN_RELEASE_ASSERT(DuplicateHandle(process, _thread.mHandle, process, &handle, 0, FALSE, DUPLICATE_SAME_ACCESS));

                if (mHandle != NULL) {
                    WN_RELEASE_ASSERT(CloseHandle(mHandle));
                }

                mHandle = handle;
            }
        #endif

        return(*this);
    }

    WN_BOOL WNThread::SetAffinity(WN_SIZE_T _affinity) {
        if (IsValid()) {
            mData->mAffinity = _affinity;

            #ifdef _WN_WINDOWS
                if (SetThreadAffinityMask(mHandle, mData->mAffinity) == 0) {
                    return(WN_FALSE);
                }
            #elif defined _WN_LINUX
                cpu_set_t affinity;

                CPU_ZERO(&affinity);

                for (WN_SIZE_T i = 0; i < (8 * (sizeof(WN_SIZE_T))); ++i) {
                    if (((mData->mAffinity >> i) & 1) == 1) {
                        CPU_SET(i, &affinity);
                    }
                }

                if (pthread_setaffinity_np(mData->mThread, sizeof(cpu_set_t), &affinity) != 0) {
                    return(WN_FALSE);
                }
            #endif
        } else {
            return(WN_FALSE);
        }

        return(WN_TRUE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::SetPriority(WN_UINT32 _priority) {
        if (IsValid()) {
            mData->mPriority = _priority;

            #ifdef _WN_WINDOWS
                if (mHandle != WN_NULL) {
                    return(SetThreadPriority(mHandle, mData->mPriority) != 0 ? WN_TRUE : WN_FALSE);
                }
            #elif defined _WN_LINUX
                return(WN_FALSE); // need to figure out
            #endif
        } else {
            return(WN_FALSE);
        }

        return(WN_TRUE);
    }

    WN_FORCE_INLINE WN_SIZE_T WNThread::GetAffinity() const {
        if (IsValid()) {
            return(mData->mAffinity);
        }

        return(0);
    }

    WN_FORCE_INLINE WN_UINT32 WNThread::GetPriority() const {
        if (IsValid()) {
            return(mData->mPriority);
        }

        return(0);
    }

    template <typename Return>
    WN_FORCE_INLINE WN_BOOL WNThread::GetReturn(Return* _return) const {
        if (IsFinished()) {
            if (mData->mReturn != WN_NULL) {
                *_return = *static_cast<Return*>(mData->mReturn);

                return(WN_TRUE);
            }
        }

        return(WN_FALSE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::IsValid() const {
        if (mData != WN_NULL) {
            if (mData->mState != eWNInvalid) {
                #ifdef _WN_WINDOWS
                    return(mHandle != NULL);
                #else
                    return(WN_TRUE);
                #endif
            }
        }

        return(WN_FALSE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::IsReady() const {
        if (mData != WN_NULL) {
            return(mData->mState == eWNReady);
        }

        return(WN_FALSE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::IsRunning() const {
        if (mData != WN_NULL) {
            return(mData->mState == eWNRunning);
        }

        return(WN_FALSE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::IsFinished() const {
        if (mData != WN_NULL) {
            return(mData->mState == eWNFinished);
        }

        return(WN_FALSE);
    }

    WN_FORCE_INLINE WN_BOOL WNThread::Join(WN_UINT32 _milliseconds) const {
        WN_DEBUG_ASSERT(_milliseconds != 0);

        if (IsValid()) {
            #ifdef _WN_WINDOWS
                if (WaitForSingleObject(mHandle, _milliseconds) == WAIT_FAILED) {
                    return(WN_FALSE);
                }
            #elif defined _WN_LINUX
                if (_milliseconds >= 0xFFFFFFFF) {
                    if (pthread_join(mData->mThread, NULL) != 0) {
                        return(WN_FALSE);
                    }
                } else {
                    struct timespec timeOut = {0};

                    timeOut.tv_sec = _milliseconds / 1000;
                    timeOut.tv_nsec = (_milliseconds % 1000) * 1000000L;

                    if (pthread_timedjoin_np(mData->mThread, WN_NULL, &timeOut) != 0) {
                        return(WN_FALSE);
                    }
                }
            #endif
        } else {
            return(WN_FALSE);
        }

        return(WN_TRUE);
    }

    WN_FORCE_INLINE WNThread::__WNThreadData::~__WNThreadData() {
        if (mParameter != WN_NULL) {
            WNMemory::WNFree(mParameter);
        }

        if (mReturn != WN_NULL) {
            WNMemory::WNFree(mReturn);
        }
    }

    template <typename Return, typename Parameter>
    WN_FORCE_INLINE WN_VOID WNThread::Create(WN_VOID* _function, WN_VOID* _parameter) {
        if (_function == WN_NULL) {
            return;
        }

        mData = WN_NEW(__WNThreadData());

        mData->AddRef();

        mData->mFunction = _function;
        mData->mParameter = _parameter;
        mData->mID = -1;
        mData->mReturn = WN_NULL;
        mData->mState = eWNInvalid;
        mData->mAffinity = 0;
        mData->mPriority = 0;

        for (WN_SIZE_T i = 0; i < WNCore::WNSystem::WNGetLogicalThreadCount(); ++i) {
            WN_BIT_SET(mData->mAffinity, i);
        }

        mData->AddRef();

        #ifdef _WN_WINDOWS
            mHandle = CreateThread(NULL, 0, &__WNThreadWrapper<Return, Parameter>::Execute, static_cast<LPVOID>(mData), 0, reinterpret_cast<LPDWORD>(&(mData->mID)));

            if (mHandle == NULL) {
                mData->RemoveRef();

                return;
            }
        #elif defined _WN_LINUX
            pthread_attr_t attributes;

            if (pthread_attr_init(&attributes) == 0) {
                const WN_UINT32 result = pthread_create(&mData->mThread, &attributes, &__WNThreadWrapper<Return, Parameter>::Execute, static_cast<void*>(mData));

                pthread_attr_destroy(&attributes);

                if (result != 0) {
                    mData->RemoveRef();

                    return;
                }
            } else {
                return;
            }
        #endif

        mData->mState = eWNReady;

        SetAffinity(mData->mAffinity);
        SetPriority(mData->mPriority);
    }

    #ifdef _WN_WINDOWS
        template <typename Return, typename Parameter>
        WN_FORCE_INLINE DWORD WINAPI WNThread::__WNThreadWrapper<Return, Parameter>::Execute(LPVOID _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            Return* result = WNMemory::WNMallocT<Return>();
            Parameter parameter = *reinterpret_cast<Parameter*>(threadData->mParameter);

            threadData->mState = eWNRunning;

            *result = reinterpret_cast<Return (*)(Parameter)>(threadData->mFunction)(parameter);

            threadData->mReturn = reinterpret_cast<WN_VOID*>(result);
            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(0);
        }

        template <typename Parameter>
        WN_FORCE_INLINE DWORD WINAPI WNThread::__WNThreadWrapper<WN_VOID, Parameter>::Execute(LPVOID _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            Parameter parameter = *reinterpret_cast<Parameter*>(threadData->mParameter);

            threadData->mState = eWNRunning;

            reinterpret_cast<WN_VOID (*)(Parameter)>(threadData->mFunction)(parameter);

            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(0);
        }

        template<>
        WN_FORCE_INLINE DWORD WINAPI WNThread::__WNThreadWrapper<WN_VOID, WN_VOID>::Execute(LPVOID _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            threadData->mState = eWNRunning;

            reinterpret_cast<WN_VOID (*)(WN_VOID)>(threadData->mFunction)();

            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(0);
        }
    #elif defined _WN_LINUX
        template <typename Return, typename Parameter>
        WN_FORCE_INLINE void* WNThread::__WNThreadWrapper<Return, Parameter>::Execute(void* _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            Return* result = WNMemory::WNMallocT<Return>();
            Parameter parameter = *reinterpret_cast<Parameter*>(threadData->mParameter);

            threadData->mID = static_cast<WN_UINT32>(syscall(SYS_gettid));
            threadData->mState = eWNRunning;

            *result = reinterpret_cast<Return (*)(Parameter)>(threadData->mFunction)(parameter);

            threadData->mReturn = static_cast<WN_VOID*>(result);
            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(NULL);
        }

        template <typename Parameter>
        WN_FORCE_INLINE void* WNThread::__WNThreadWrapper<WN_VOID, Parameter>::Execute(void* _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            Parameter parameter = *reinterpret_cast<Parameter*>(threadData->mParameter);

            threadData->mID = static_cast<WN_UINT32>(syscall(SYS_gettid));
            threadData->mState = eWNRunning;

            reinterpret_cast<WN_VOID (*)(Parameter)>(threadData->mFunction)(parameter);

            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(NULL);
        }

        template<>
        WN_FORCE_INLINE void* WNThread::__WNThreadWrapper<WN_VOID, WN_VOID>::Execute(void* _threadData) {
            __WNThreadData* threadData = reinterpret_cast<__WNThreadData*>(_threadData);

            threadData->mID = static_cast<WN_UINT32>(syscall(SYS_gettid));
            threadData->mState = eWNRunning;

            reinterpret_cast<WN_VOID (*)()>(threadData->mFunction)();

            threadData->mState = eWNFinished;

            if (threadData->RemoveRef()) {
                WN_DELETE(threadData);
            }

            return(NULL);
        }
    #endif

    WN_FORCE_INLINE WN_VOID WNThreadYield() {
        if (WNCore::WNSystem::WNGetLogicalThreadCount() > 1) {
            WN_YIELD_PROCESSOR();
        } else {
            #ifdef _WN_WINDOWS
                SwitchToThread();
            #elif defined _WN_LINUX
                pthread_yield();
            #endif
        }
    }

    WN_FORCE_INLINE WN_VOID WNThreadSleep(WN_UINT32 _milliseconds) {
        #ifdef _WN_WINDOWS
            Sleep(_milliseconds);
        #elif defined _WN_LINUX
            struct timespec timeRequested = {0};

            timeRequested.tv_sec = _milliseconds / 1000;
            timeRequested.tv_nsec = (_milliseconds % 1000) * 1000000L;

            while (nanosleep(&timeRequested, &timeRequested) == -1) {
                continue;
            }
        #endif
    }
}

#endif // __WN_CONCURRENCY_THREAD_INL__
